문단 클릭 = 시작 위치 선택 · 재생은 우하단 버튼/P (재생 중 클릭 = 점프)

5장. 지식 그래프를 활용한 그래프 RAG

출처 — 브라이스 유·조경아·박수진·김재웅, 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(프리렉, 2025), 5장 (pp. 299~366). 원문 PDF rag_master_final_v11_260910.pdf (2026-09-10 판)

기존 RAG는 문서 조각을 개별로 검색할 뿐 정보 사이의 관계를 보지 못한다 — 그래프 RAG는 지식 그래프로 그 관계를 구조화해, 좁은 질문은 로컬 검색으로 세부까지, 문서 전체를 아우르는 질문은 글로벌 검색으로 포괄적으로 답한다.

학습 목표

이 장을 끝내면 다음을 할 수 있다.

  • 기존 RAG 방식의 두 가지 한계(정보 연결의 어려움·대규모 데이터 이해의 한계)를 설명한다.
  • 그래프와 지식 그래프의 차이, 지식 그래프의 세 가지 특징(복잡한 정보 구조·추론 능력·효율적 검색)을 구분한다.
  • 그래프 DB 구축 4단계(문서 분할·지식 그래프 구축·그래프 증강·커뮤니티 요약)의 입출력을 설명한다.
  • 로우벤·레이든 두 커뮤니티 탐지 알고리즘의 동작 방식과 장단점을 비교한다.
  • 마이크로소프트 GraphRAG 라이브러리로 그래프 DB를 구축하고, 질문의 성격에 따라 로컬 검색과 글로벌 검색 중 알맞은 방식을 선택해 질의한다.
  • Neo4j와 랭체인을 결합해 GraphRAG 산출물을 그래프 데이터베이스에 저장하고 로컬·글로벌 검색기를 직접 구현한다.

전체 흐름도

[ 5장 — 지식 그래프를 활용한 그래프 RAG ]
      │
      ├─ §1 그래프 RAG 개요 ── "기존 RAG는 왜 한계에 부딪히나"
      │       ├─ 기존 RAG 한계 ① 정보 연결의 어려움 ② 대규모 데이터 이해의 한계
      │       ├─ 그래프(노드·에지, 방향성·가중치) → 지식 그래프(의미+속성 부여)
      │       │       └─ 3특징 — 복잡한 정보 구조 · 추론 능력 · 효율적 검색
      │       └─ 그래프 RAG 동작 개관 — 그래프 DB 구축 → 로컬/글로벌 검색
      ▼
      ├─ §2 그래프 DB 구축 — 이론 ── "문서를 어떻게 그래프로 바꾸나" (4단계)
      │       ├─ 1. 문서 분할 — 청크 크기 ↔ 품질 트레이드오프 (600 vs 2400 토큰 실험)
      │       ├─ 2. 지식 그래프 구축 — 규칙 기반 → 머신러닝 → 딥러닝 → LLM(현재 주류)
      │       │       └─ 엔티티·관계 추출 → 그래프 통합(중복 제거) → 설명 요약 → 주장 추출(선택)
      │       │              └─ Gleaning(이삭 줍기) — 반복 추출로 누락 보완
      │       ├─ 3. 그래프 증강 — 커뮤니티 탐지(로우벤·레이든, 계층 Level 0~2)
      │       │       │                    + 그래프 임베딩(Node2vec — 랜덤워크 p·q)
      │       └─ 4. 커뮤니티 요약 — 연결 정도(중요도) 순 정렬 → LLM 요약 → 임베딩
      ▼
      ├─ §3 그래프 DB 구축 실습 — GraphRAG 라이브러리 ── "직접 만들어 본다"
      │       └─ 환경설정(pip install graphrag) → 초기화(.env·settings.yaml)
      │              → graphrag index 실행
      │              ├─ Base processing — 문서분할·엔티티추출·기본 그래프·공변량
      │              └─ Enrichment — 정제·시각화 좌표·커뮤니티 요약 (7단계 워크플로우)
      ▼
      ├─ §4 그래프 RAG 질의 ── "구축한 그래프로 어떻게 답하나"
      │       ├─ 로컬 검색 — 질문 임베딩 → 관련 엔티티 식별 → 5종 정보 수집 → 리랭킹
      │       │       └─ (세부 질문에 강함 — "산업채권 순유동자산 분석 방법")
      │       ├─ 글로벌 검색 — 커뮤니티 레벨 선택 → 맵(중간 응답) → 리듀스(통합)
      │       │       └─ (포괄 질문에 강함 — "채권의 전환성이 각 챕터에서 어떻게 소개됐나")
      │       └─ 같은 질문을 두 방식으로 실행해 답변의 범위·깊이 차이를 직접 비교
      ▼
      └─ §5 Neo4j와 랭체인을 활용한 GraphRAG 구현 ── "결과물을 영구 저장·재사용"
              ├─ GraphRAG의 Parquet 출력 → Neo4j 노드·관계로 배치 임포트
              │       (문서 → 청크 → 엔티티 → 관계 → 커뮤니티 → 커뮤니티 보고서)
              └─ 랭체인 연동 — 로컬 검색기(벡터 유사도 + fetch_entity_context)
                             글로벌 검색기(맵-리듀스 체인 + Neo4jGraph 쿼리)

0. 용어 사전

참고 — 위쪽 4개는 이 장을 읽기 전에 알아야 하는 선행 용어다. 이 장은 임베딩 기반 벡터 검색, 리랭킹, LLM 프롬프트 작성을 이미 아는 것을 전제로 설명하기 때문이다.

한글 용어 원문 영문명 의미
임베딩 Embedding (선행) 텍스트를 고정 길이 숫자 벡터로 바꾸는 것. 의미가 비슷한 텍스트는 벡터 공간에서도 가깝게 위치한다. 책을 주제별 좌표에 꽂아 두는 도서관 사서에 비유. 본문 §2·§5
벡터 검색(유사도 검색) Vector Search / Similarity Search (선행) 질문을 임베딩한 뒤 저장된 벡터들과의 거리(코사인 유사도 등)를 계산해 가장 가까운 것을 찾는 방법. 본문 §4·§5
리랭킹 Reranking (선행) 1차로 모아온 후보 정보를 실제 관련성 기준으로 다시 순위 매겨 상위 일부만 남기는 후처리 과정. 본문 §4
LLM 프롬프트 Prompt (선행) 언어 모델에 원하는 작업을 지시하는 입력 텍스트. 이 장은 엔티티 추출·요약·질의응답을 전부 프롬프트로 지시한다. 본문 §2·§3·§4
지식 그래프 Knowledge Graph 개체(노드)와 그 사이의 관계(에지)에 의미론적 정보(타입·속성)를 부여해 구조화한 그래프. 본문 §1
노드·에지 Node / Edge 그래프를 구성하는 두 요소를 가리키는 수학적 용어. 지식 그래프에서는 각각 개체·관계라 부른다. 본문 §1
엔티티 Entity 지식 그래프의 노드에 해당하는 개체. 이름(name)·타입(type)·설명(description)을 갖는다. 본문 §1·§2
관계 Relationship 두 엔티티를 잇는 에지. 출발점(source)·도착점(target)·설명·강도(1~10)를 갖는다. 본문 §1·§2
커뮤니티 Community 지식 그래프 안에서 서로 밀접하게 연관된 엔티티들의 그룹. 검색 범위를 좁혀 효율을 높인다. 본문 §2
커뮤니티 탐지 Community Detection 그래프 이론 알고리즘으로 커뮤니티를 찾아내는 과정. 대표적으로 로우벤·레이든 알고리즘이 있다. 본문 §2
모듈성 Modularity 그룹 내부 연결 밀도를 나타내는 지표. 높을수록 "그룹 안은 촘촘, 그룹 밖은 성김"을 뜻하며 커뮤니티 탐지의 최적화 기준이 된다. 본문 §2
로우벤 알고리즘 Louvain Algorithm 모듈성 변화를 반복 계산해 커뮤니티를 나누는 알고리즘. 빠르고 구현이 간단하지만 결과가 불안정할 수 있다. 본문 §2
레이든 알고리즘 Leiden Algorithm 로우벤의 비연결 커뮤니티 문제를 해결한 2018년 개선판. 정제 단계를 추가해 더 세밀하고 안정적인 커뮤니티를 만든다. 본문 §2
계층적 커뮤니티 Hierarchical Community 레이든 알고리즘으로 만드는 Level 0~2의 다단계 커뮤니티 구조. 레벨이 깊을수록 더 세부적인 주제를 담는다. 본문 §2
그래프 임베딩 Graph Embedding 그래프의 구조적 특성(노드 간 연결 관계)을 벡터로 변환하는 것. 텍스트 의미를 벡터화하는 엔티티 임베딩과는 대상이 다르다. 본문 §2·§3
Node2vec Node2vec 랜덤워크로 수집한 이웃 노드 집합(컨텍스트)을 단어 임베딩 기법처럼 벡터화하는 그래프 임베딩 기법. 본문 §2
랜덤워크 Random Walk 그래프에서 임의의 경로를 따라 이동하며 노드를 방문하는 과정. Node2vec이 컨텍스트를 모으는 방법. 본문 §2
p·q 파라미터 Return Parameter / In-out Parameter Node2vec의 랜덤워크 탐색 방향을 조절하는 하이퍼파라미터. p는 이전 노드로 돌아갈 확률, q는 폭(BFS)·깊이(DFS) 탐색의 균형을 정한다. 본문 §2
Gleaning Gleaning(이삭 줍기) LLM 출력을 반복적으로 검증·보완해 정보 추출의 완전성을 높이는 기법. 본문 §2
공변량 Covariate 텍스트에서 추출한 주장(claim)을 상태(status)·시작일·종료일과 함께 구조화한 것. 통계·머신러닝에서 종속 변수에 영향을 미치는 독립 변수를 뜻하는 말을 그래프 RAG에 맞게 가져왔다. 본문 §2
로컬 검색 Local Search 질문과 직접 관련된 엔티티를 중심으로 상세한 정보를 모아 답하는 검색 방식. 세부적인 질문에 적합하다. 본문 §1·§4·§5
글로벌 검색 Global Search 문서 전체를 아우르는 포괄적 질문에, 맵-리듀스 방식으로 커뮤니티 요약들을 종합해 답하는 검색 방식. 본문 §1·§4·§5
맵-리듀스 Map-Reduce 여러 조각(커뮤니티 리포트)에서 중간 응답을 만든 뒤(맵) 중요도 높은 것을 통합해(리듀스) 최종 답을 만드는 두 단계 처리 방식. 본문 §4·§5
GraphRAG(라이브러리) GraphRAG (Microsoft) 마이크로소프트가 공개한 그래프 RAG 파이프라인 오픈소스 패키지. 문서 → 지식 그래프 → 커뮤니티 요약까지의 과정을 자동화한다. 본문 §3
Parquet Apache Parquet GraphRAG가 엔티티·관계·커뮤니티 등 산출물을 저장하는 컬럼 지향 파일 형식. 본문 §3·§5
Neo4j Neo4j 노드와 관계로 데이터를 표현하는 그래프 데이터베이스 시스템. Cypher 쿼리 언어를 쓴다. 본문 §5
Cypher Cypher Neo4j의 그래프 전용 쿼리 언어. MATCH(패턴 검색)·CREATE(생성)·MERGE(생성 또는 갱신)·SET(속성 설정) 등을 쓴다. 본문 §5
Neo4j Aura Neo4j Aura Neo4j의 완전관리형 클라우드 인스턴스 서비스. 본문 §5

1. 그래프 RAG 개요

2장에서 다룬 기존 RAG 방식은 질문과 의미가 비슷한 문서 조각을 찾아오는 데는 강하지만, 조각과 조각 사이의 관계를 보지는 못한다. 이 절에서는 그 한계를 구체적인 두 가지 사례로 확인하고, 그래프와 지식 그래프라는 개념을 거쳐 그래프 RAG가 무엇을 다르게 하는지 살펴본다.

기존 RAG 방식의 두 가지 한계

정보 연결의 어려움. 기존 RAG는 개별 문서에서 특정 소주제나 키워드를 검색할 수 있지만, 데이터 간의 관계를 분석해 새로운 통찰을 제공하는 데는 한계가 있다. 예를 들어 기업의 사업보고서 여러 건에 "이 문서의 주요 주제는 무엇인가요?"라고 물으면, 한 문서에서 사업 영역을, 다른 문서에서 재무 상태를 각각 찾아올 수는 있어도 그 정보들이 데이터셋 전체에서 어떻게 연결되고 어떤 전반적인 패턴을 이루는지는 알지 못한다.

대규모 데이터 이해의 한계. 데이터 규모가 커질수록 기존 RAG는 개념 전체를 파악하는 데 더 어려움을 겪는다. "지난 10년간의 연간 보고서를 바탕으로 회사의 지속가능성 전략 변화를 설명해 주세요" 같은 질문에는 연도별 개별 문서를 검색해 올 수는 있지만, 그 문서들이 서로 어떻게 연결되고 전략이 시간에 따라 어떻게 변해 왔는지 추적하지는 못한다.

이 두 한계는 결국 하나의 원인으로 모인다 — 기존 RAG는 문서 조각을 독립적으로 검색할 뿐, 조각 사이의 구조를 만들지 않는다는 것이다. 이를 해결하기 위해 그래프 RAG가 제안되었다. 그래프 RAG는 전체 텍스트 코퍼스에 대한 질문에 답하기 위해 지식 그래프와 검색 증강 생성(RAG)을 결합한 접근 방식으로, 개별 정보 조각 사이의 관계를 모델링해 데이터셋 전체의 연결성과 패턴을 파악할 수 있게 한다.

그래프와 지식 그래프

그래프는 네트워크 구조를 수학적으로 표현한 모델로, 상호 연결된 요소들 간의 관계를 나타낸다. 소셜 미디어의 친구·팔로우 관계, 도로·철도로 이어진 교통망, 컴퓨터와 서버가 정보를 주고받는 인터넷까지 — 우리 주변의 네트워크는 모두 "개체"와 그 사이의 "관계"로 이루어진다. 수학적 맥락에서는 노드(정점)·에지(간선)라는 용어를 쓰고, 응용 분야에서는 개체·관계로 표현하는 경향이 있다. 그래프는 단순한 연결뿐 아니라 방향성(SNS의 팔로우처럼 한쪽에서 다른 쪽으로만 향하는 관계)과 가중치(도시 간 거리처럼 연결의 강도)도 표현할 수 있다.

지식 그래프는 이 기본 구조 위에 의미를 더한 특별한 형태의 그래프다. 일반 그래프가 "아이폰"과 "애플"이 단순히 연결선으로 이어져 있다고만 표현한다면, 지식 그래프는 그 연결을 "개발(developedBy)"이라는 명확한 관계로 정의해 "아이폰이 애플에 의해 개발되었다"는 사실을 구체적으로 나타낸다. 또한 "아이폰"에는 출시일(2007년)·운영체제(iOS)·디스플레이 크기 같은 속성을, "개발" 관계 자체에도 개발 시기·조건 같은 속성을 추가할 수 있다.

이러한 구조 덕분에 지식 그래프는 일반 그래프와 구별되는 세 가지 특징을 지닌다.

① 복잡한 정보 구조 표현. 일반 그래프의 연결선은 "이 둘이 어떤 관계를 맺고 있다"는 사실만 나타내지만, 지식 그래프는 그 연결에 "개발(developedBy)" 같은 명확한 의미를 부여해 기계가 관계를 해석하고 활용할 수 있게 한다. 여기에 엔티티·관계 각각에 세부 속성까지 더하면, "애플이 아이폰을 개발했다"는 사실 이상으로 풍부한 정보를 담을 수 있다.

② 추론 능력. "스티브 잡스가 애플을 공동 창업했다"와 "스티브 잡스가 픽사의 CEO였다"라는 두 문장은 개별로 보면 애플과 픽사 사이의 연관성을 알기 어렵다. 하지만 이를 지식 그래프로 표현하면 '스티브 잡스'라는 노드를 중심으로 애플과 픽사가 연결되어 있음이 한눈에 드러나, 두 회사의 혁신적인 기업 문화 같은 공통점까지 추론할 수 있다.

③ 효율적인 정보 검색. 노드와 에지의 연결을 따라가면 관련 정보를 빠르게 탐색할 수 있다. 예를 들어 '애플' 노드에서 시작하면 이웃 노드인 '맥북', '스티브 잡스', '팀 쿡'을 바로 찾아낼 수 있다. 검색 엔진·추천 시스템·질의응답 시스템이 지식 그래프를 널리 쓰는 이유다.

그래프 RAG의 동작 과정

그래프 RAG 시스템은 크게 두 국면으로 움직인다. 먼저 그래프 DB 구축 단계에서 문서 내용을 분석해 개체와 관계를 추출하고, 밀접하게 연관된 개체들의 그룹(커뮤니티)을 찾아 각 커뮤니티의 핵심 내용을 요약해 둔다. 이후 사용자가 질문하면 질문의 성격에 따라 두 검색 방식 중 하나를 택한다.

  • 로컬 검색(Local Search) — 질문과 직접 관련된 개체들을 식별하고 그 주변 정보를 수집해 상세한 답변을 만든다. 특정 주제나 세부 정보를 묻는 질문에 효과적이다.
  • 글로벌 검색(Global Search) — 문서 전체를 아우르는 포괄적 질문에, 여러 커뮤니티 요약을 종합해 답한다.

이 두 국면(그래프 DB 구축·검색)을 §2~§5에서 이론과 실습으로 차례로 살펴본다. 이번 장 내용은 마이크로소프트의 GraphRAG 연구(Edge et al., 2024)를 참고해 작성되었다.

2. 그래프 DB 구축 — 이론

그래프 DB 구축 과정은 네 단계로 이루어진다 — 문서 분할 → 지식 그래프 구축 → 그래프 증강 → 커뮤니티 요약. 각 단계는 앞 단계의 산출물을 입력으로 받아, 단순한 텍스트 뭉치를 점점 더 구조화되고 의미적으로 연결된 지식 네트워크로 바꿔 나간다.

1) 문서 분할

가장 먼저 문서를 적절한 크기로 나눈다. 이 길이 설정에는 트레이드오프가 있다 — 문서가 길수록 LLM 호출 횟수가 적어 비용은 줄지만 추출되는 지식 그래프의 품질은 낮아질 수 있고, 짧을수록 비용은 늘지만 품질은 높아질 수 있다. 실제로 문서를 600토큰 수준으로 분할했을 때가 2400토큰 수준으로 분할했을 때보다 감지되는 엔티티 수가 거의 두 배 가까이 늘었다는 연구 결과가 있다. 그래서 문서 분할은 가용 자원과 원하는 품질 수준을 함께 고려해 정해야 한다.

2) 지식 그래프 구축

분할된 문서에서 엔티티와 관계를 추출해 기본 그래프 구조를 만드는 단계다. 이때 쓰이는 기술은 시대에 따라 바뀌어 왔다.

  • 규칙 기반 접근법 — 미리 정의한 언어 패턴으로 추출한다. 예를 들어 "회사명 + 은/는 + 제품명 + 을/를 출시했다"라는 패턴을 두면 "삼성전자는 갤럭시 S23을 출시했다"에서 '삼성전자'·'갤럭시 S23'을 엔티티로, '출시'를 관계로 뽑아낸다. 구현은 간단하지만 패턴에 정확히 들어맞는 문장에서만 동작해 확장성과 유연성이 부족하다.
  • 기계학습 모델 — SVM·CRF 같은 알고리즘으로 문맥 정보에 기반해 추출한다. 규칙 기반보다 정확도는 높고 구동 자원은 적게 들지만, 최신 딥러닝만큼 복잡한 언어 표현을 처리하지는 못한다.
  • 딥러닝 모델 — RNN·트랜스포머 같은 신경망으로 추출한다. BERT 같은 트랜스포머 기반 모델은 1억 개 이상의 파라미터로 더 깊은 문맥 이해가 가능하지만, 학습에 대규모 데이터셋과 자원이 필요하다.
  • 대규모 언어 모델(LLM) — 오늘날 가장 널리 쓰이는 방식이다. 사전 학습된 LLM은 별도의 모델 학습 없이도 높은 정확도로 엔티티와 관계를 추출한다. 다만 컴퓨팅 자원·API 비용이 들고 처리 속도가 상대적으로 느리며, 출력의 일관성이 완벽하지 않아 후처리가 필요할 수 있다.

LLM을 활용한 추출 예시. "테슬라는 일론 머스크가 설립한 회사로, 전기차와 태양광 패널을 생산합니다"라는 문장에 "다음 글에서 엔티티의 이름, 타입, 설명을 추출하고, 엔티티들 간의 관계를 설명과 함께 1~10 사이의 강도로 표시하세요"라는 프롬프트를 준다면, LLM은 대략 다음과 같은 출력을 낸다.

엔티티:
1. "테슬라" (ORGANIZATION, 전기차와 친환경 에너지를 생산하는 기업)
2. "일론 머스크" (PERSON, 테슬라의 설립자이자 현 CEO)
3. "전기차" (PRODUCT, 테슬라가 생산하는 주력 제품)
4. "태양광 패널" (PRODUCT, 테슬라가 생산하는 친환경 에너지 제품)

관계:
1. "일론 머스크" -> "테슬라" (설립자이자 CEO로 회사를 이끌고 있음, 강도: 9)
2. "테슬라" -> "전기차" (주력 제품으로 설계 및 생산, 강도: 8)
3. "테슬라" -> "태양광 패널" (친환경 에너지 제품 라인의 일부로 생산, 강도: 7)

참고 — Gleaning을 통한 지식 그래프 품질 향상. Gleaning(이삭 줍기)은 LLM의 출력을 반복적으로 검증하고 보완하는 기법이다. 농작물을 수확한 뒤 남은 이삭이 있는지 다시 한번 확인하듯, 초기 추출 이후 누락되거나 불완전한 정보를 보완하는 과정을 반복한다. 마이크로소프트의 연구에 따르면 Gleaning을 세 번 수행했을 때 초기 추출 대비 훨씬 많은 엔티티를 탐지할 수 있었고, 더 큰 청크 크기를 쓰더라도 Gleaning으로 성능 저하를 상당 부분 보완할 수 있었다. 적용 절차는 다음과 같다. ① 초기 추출 단계의 엔티티·관계 정보를 LLM에 다시 입력해 누락된 정보가 있는지 검토한다. ② 누락이 식별되면 추가 질의로 보완 정보를 수집한다. ③ 일정 기준에 도달할 때까지 이 과정을 반복해 정보의 완전성을 점진적으로 높인다.

지식 그래프 생성은 구체적으로 다음 네 단계로 진행된다.

① 엔티티와 관계 추출. 분할된 각 문서에서 엔티티(이름·타입·설명)와 관계(출발점·도착점·설명·강도)를 추출한다. 예를 들어 "일론 머스크는 2022년에 수백억 달러로 트위터를 인수했습니다"라는 문장에서는 이름 "ELON MUSK"(타입 PERSON, 설명 "트위터를 인수한 기업인")와 이름 "TWITTER"(타입 ORGANIZATION, 설명 "2022년 수백억 달러에 인수된 기업")라는 두 엔티티, 그리고 두 엔티티를 잇는 "2022년 수백억 달러에 인수함"(강도 9)이라는 관계가 추출된다.

② 그래프 통합. 추출된 엔티티와 관계를 통합해 중복을 제거한다. 동일한 이름과 타입의 엔티티는 하나로 합치고, 각각의 설명은 배열로 저장한다. 예를 들어 "ELON MUSK"라는 이름의 엔티티가 "트위터를 인수한 기업인"이라는 설명과 "스페이스X를 설립한 기업인"이라는 설명으로 각각 추출되었다면, 통합 후에는 이름: "ELON MUSK", 타입: "PERSON", 설명: ["트위터를 인수한 기업인", "스페이스X를 설립한 기업인"] 형태가 된다. 동일한 출발점·도착점을 가진 관계들도 같은 방식으로 통합된다.

③ 설명 요약. LLM을 활용해 통합된 배열 형태의 설명들을 하나의 간결한 설명으로 요약한다. 앞의 예라면 ["트위터를 인수한 기업인", "스페이스X를 설립한 기업인"]이 "스페이스X를 설립하고 트위터를 인수한 기업인"이라는 하나의 문장으로 정리된다.

④ 주장 추출(선택사항). 텍스트에서 사실적 진술을 뽑아 상태(status)와 시간(time) 정보를 포함한 구조화된 주장(claim)으로 바꾸는 선택적 단계다. "2022년 10월 27일, 일론 머스크가 440억 달러를 들여 트위터를 인수했습니다"라는 문장에서는 다음과 같은 주장이 추출된다.

유형: ACQUISITION
설명: 일론 머스크가 트위터를 440억 달러에 인수함
주체식별자: ELON MUSK
객체식별자: TWITTER
상태: TRUE
시작일자: 2022-10-27T00:00:00
종료일자: 2022-10-27T00:00:00
출처: 일론 머스크가 440억 달러를 들여 트위터를 인수했습니다.

이렇게 추출된 주장은 공변량(covariate) 형태로 저장되어, 시간적 맥락이나 상태 변화를 반영한 고도화된 질의응답에 쓰인다.

3) 그래프 증강

지식 그래프를 만들었다면, 이제 구조를 더 풍부하게 만들고 실제로 활용 가능한 수준으로 발전시키는 증강 과정이 필요하다. 이 과정은 커뮤니티 탐지그래프 임베딩이라는 두 방향으로 진행된다.

커뮤니티 탐지. 커뮤니티란 지식 그래프 안에서 서로 밀접하게 연관된 엔티티들의 그룹이다. 방대한 그래프에서 모든 엔티티를 일일이 살피는 것은 비효율적이므로, '애플'·'아이폰'·'맥북'을 '애플 제품' 커뮤니티로, '구글'·'안드로이드'·'픽셀'을 '구글 제품' 커뮤니티로 묶으면 검색과 처리가 훨씬 빨라진다. 대표적인 알고리즘 둘을 살펴보자.

로우벤 알고리즘(Louvain algorithm)은 그룹 내부 연결 밀도를 나타내는 모듈성(modularity)을 계산해 커뮤니티를 나눈다. 모듈성이 높다는 것은 "그룹 안은 많이 연결되어 있고, 다른 그룹과는 적게 연결되어 있다"는 뜻이다. 동작 방식은 다음과 같다.

  1. 초기화 — 모든 노드가 각자 하나의 커뮤니티로 시작한다.
  2. 그룹화 및 모듈성 계산 — 각 노드를 이웃 노드의 그룹으로 이동시켜 보며 모듈성 변화를 계산하고, 가장 많이 증가하는 그룹으로 이동시킨다(증가가 없으면 그대로 둔다).
  3. 반복 및 병합 — 2번을 모든 노드에 반복하고, 더 이상 개선이 없으면 형성된 그룹을 하나의 노드로 간주해 네트워크를 재구성한 뒤 다시 2번을 수행한다.
  4. 종료 — 전체 모듈성이 더 이상 증가하지 않으면 멈춘다.

로우벤 알고리즘은 계산이 빠르고 구현이 간단해 대규모 네트워크에도 잘 맞지만, 초기 설정이나 노드 처리 순서에 따라 결과가 달라지는 불안정성과, 실제로는 존재하지 않는 커뮤니티를 찾아내기도 하는 모듈성 함수의 한계를 갖는다.

레이든 알고리즘(Leiden algorithm)은 2018년에 소개된 로우벤의 개선판이다(Traag et al., 2019). 로우벤은 노드를 커뮤니티로 옮길 때 그 커뮤니티 내부가 서로 연결되지 않은 상태로 묶이는 문제가 있었는데, 레이든은 이동 후에도 커뮤니티가 항상 서로 연결된 상태를 유지하도록 설계해 이 문제를 해결했다. 또한 커뮤니티 병합 전에 정제 단계를 추가해 더 세밀한 하위 커뮤니티 구조를 형성하고 잘못 배치된 노드를 재배치한다. 그 결과 대규모 네트워크에서도 안정적이고 정확한 커뮤니티를 찾아내지만, 모듈성 함수 자체의 한계(해상도 한계 등)는 여전히 남아 있고 정제 단계 때문에 반복당 계산량이 늘 수 있다. 다만 더 적은 반복으로도 더 나은 결과를 얻을 수 있어 전반적으로는 로우벤보다 효율적인 경우가 많다.

마이크로소프트의 GraphRAG는 여기서 한 발 더 나아가 레이든 알고리즘으로 Level 0~2의 계층적 커뮤니티 구조를 만든다. 테슬라 관련 뉴스 데이터를 예로 들면, Level 0은 '전기차 산업' 같은 광범위한 주제, Level 1은 'Tesla'·'SpaceX'·'Twitter(X)' 같은 기업별 커뮤니티, Level 2는 각 기업의 제품 라인업·주요 인물·주요 사건 같은 세부 정보로 나뉜다. "테슬라의 전반적인 현황"처럼 광범위한 질문에는 Level 1의 요약을, "테슬라의 신규 공장 건설 현황"처럼 구체적인 질문에는 Level 2의 상세 정보를 쓰는 식으로 질문의 범위·깊이에 맞는 레벨을 골라 쓸 수 있다. 계층 구조의 이점은 두 가지다 — 정보의 효율적 조직화(레벨마다 적절한 상세도 제공)와 LLM 토큰 제한 관리(질문 범위에 맞는 레벨만 골라 써서 토큰을 절약).

그래프 임베딩. 커뮤니티까지 찾았다면, 다음은 그래프의 구조적 특성과 노드 간 관계를 벡터(정량적 수치)로 바꾸는 그래프 임베딩 차례다. 임베딩 방법은 크게 세 갈래로 나뉜다.

  • 행렬 분해 기반 — 인접 행렬·라플라시안 행렬을 분해해 임베딩을 얻는다(예: Laplacian Eigenmaps).
  • 확률적 방법 — 랜덤워크나 확률 모델로 노드 간 유사성을 추정한다(예: DeepWalk, Node2vec).
  • 딥러닝 기반 — 그래프 신경망(GNN)으로 그래프 구조를 학습한다(예: GCN, GAT).

이 중 마이크로소프트의 GraphRAG가 채택한 것은 Node2vec이다. 수만 개 이상의 노드·에지를 가진 대규모 문서 그래프에서 지역적·전역적 구조를 모두 고려해야 하는 그래프 RAG의 특성을 잘 반영하기 때문이다. Node2vec의 핵심 아이디어는 두 가지다 — ① 비슷한 컨텍스트를 가진 노드는 비슷한 임베딩을 갖는다. ② 각 노드의 컨텍스트는 랜덤워크로 수집된 이웃 노드의 집합으로 정의된다.

참고 — Node2vec의 p와 q 파라미터. p(Return Parameter)는 이전 노드로 돌아갈 확률을 제어한다. p가 클수록 새 경로를 탐색할 확률이 높아지고, 작을수록 이전 노드로 되돌아갈 확률이 높아진다. q(In-out Parameter)는 탐색의 폭과 깊이를 결정한다. q가 1보다 크면 너비 우선 탐색(BFS)에 가까워져 주변 이웃을 폭넓게 탐색하고, 1보다 작으면 깊이 우선 탐색(DFS)에 가까워져 더 멀리 있는 노드를 방문할 가능성이 높아진다.

랜덤워크로 만든 노드 시퀀스는 마치 한 문장 안의 단어들처럼 다뤄져, 특정 노드(단어) 주변에 어떤 노드(단어)가 자주 함께 등장하는지를 단어 임베딩 기법으로 벡터화한다. 이렇게 얻은 벡터는 해당 노드의 역할과 특성을 반영하며, 비슷한 역할을 하는 노드들은 벡터 공간에서도 서로 가깝게 위치한다.

4) 커뮤니티 요약

마지막 단계에서는 앞서 만든 커뮤니티들의 핵심 엔티티·관계·주요 주장을 압축한 요약문을 만든다. 이 요약문은 이후 검색 과정에서 전체 문서를 뒤지는 대신 관련성 높은 커뮤니티 요약만 참조하게 해 검색 효율을 크게 높인다.

절차는 두 단계다. 먼저 커뮤니티를 구성하는 요소들을 중요도에 따라 정렬한다 — 이때 우선순위 기준은 각 엔티티가 다른 엔티티들과 얼마나 많이·긴밀하게 연결되어 있는가를 나타내는 연결 정도(degree)다. 그다음 우선순위가 높은 정보부터 순서대로 LLM에 넣어 요약문을 생성하고, 토큰 제한에 도달할 때까지 이 과정을 반복한다. 이렇게 만든 커뮤니티 요약문은 다시 임베딩 벡터로 변환되어, 사용자 질의와의 유사도를 빠르게 계산하는 데 쓰인다. 계층 구조 덕분에 상위 레벨에서 하위 레벨로 점진적인 검색도 가능해, 대규모 문서 컬렉션에서도 효율적인 정보 접근이 이루어진다.

3. 그래프 DB 구축 실습 — GraphRAG 라이브러리

이제 마이크로소프트의 GraphRAG 패키지로 실제 그래프 DB를 구축해 본다. 실습 코드는 5장 폴더의 ch05_GRAPHRAG_DB구축.ipynb다. GraphRAG는 이 책이 쓰인 시점에도 활발히 개발되던 패키지였다는 점을 원문이 직접 언급하고 있다 — 실제로 이후 어떻게 바뀌었는지는 이 장 끝의 최신 동향에서 다룬다.

환경 설정

GraphRAG에는 Python 3.10 이상이 필요하다. 먼저 패키지를 설치한다.

!pip install graphrag

그다음 pathlib로 작업 디렉터리를 만든다.

from pathlib import Path

working_dir = Path("working_directory")
working_dir.mkdir(exist_ok=True)

작업 디렉터리를 초기화하면 .envsettings.yaml 두 설정 파일이 생성된다. .env에는 사용할 LLM의 API 키를 입력한다.

GRAPHRAG_API_KEY=여러분의 API 키

settings.yaml 설정

settings.yaml은 GraphRAG의 데이터 처리·LLM 설정·병렬 처리를 담은 전체 설정 파일이다. 주요 항목을 하나씩 살펴본다.

원문 코드 — 대규모 언어 모델 설정(models). API 키와 사용할 모델 종류를 지정한다.

models:
  api_key: ${GRAPHRAG_API_KEY}
  type: openai_chat   # 또는 azure_openai_chat
  model: gpt-4-turbo-preview   # 책 집필 시점 예시 모델명. 최신 모델 지정은 이 장 끝 최신 동향 참고

type에는 사용할 모델의 유형을 지정한다. 여기서는 오픈AI의 채팅 모델을 쓰므로 openai_chat으로 설정하고, api_key에 API 키를 넣는다.

원문 코드 — 문서 분할 설정(chunks). 대용량 텍스트를 청크로 나누는 기준이다.

chunks:
  size: 300
  overlap: 100
  group_by_columns: [id]

group_by_columns는 지정된 칼럼별로 문서를 그룹화해 청크를 만드는 옵션이다.

원문 코드 — 엔티티 추출 설정(extract_graph). §2에서 다룬 「엔티티와 관계 추출」 단계를 수행하는 프롬프트와 대상 타입을 지정한다.

extract_graph:
  prompt: "prompts/extract_graph.txt"
  entity_types: [organization, person, geo, event]
  max_gleanings: 1

prompt는 LLM에 전달할 프롬프트 파일 위치, entity_types는 추출 대상 엔티티 종류, max_gleanings는 §2의 Gleaning 반복 횟수다.

원문 코드 — 설명 요약 설정(summarize_descriptions). §2의 「설명 요약」 단계에 대응한다.

summarize_descriptions:
  model_id: default_chat_model
  prompt: "prompts/summarize_descriptions.txt"
  max_length: 500

같은 엔티티에 설명이 여러 개일 때 하나로 통합하는 단계의 설정이며, max_length가 요약문의 최대 길이를 제한한다.

원문 코드 — 주장 추출 설정(extract_claims). §2의 「주장 추출(선택사항)」 단계다.

extract_claims:
  enabled: false
  model_id: default_chat_model
  prompt: "prompts/extract_claims.txt"
  description: "Any claims or facts that could be relevant to information discovery."
  max_gleanings: 1

enabled로 활성화 여부를 정한다. 기본값은 false이며, 주장 추출은 선택사항이라는 §2의 설명과 일치한다.

원문 코드 — 커뮤니티 요약문 설정(community_reports). §2의 「커뮤니티 요약」 단계에 대응한다.

community_reports:
  model_id: default_chat_model
  graph_prompt: "prompts/community_report_graph.txt"
  text_prompt: "prompts/community_report_text.txt"
  max_length: 2000
  max_input_length: 8000

지식 그래프 구축 실행

설정을 마쳤다면 working_directory 안에 input 폴더를 만들고 그래프 DB로 쓸 문서를 넣는다.

input_dir = working_dir / "input"
input_dir.mkdir(parents=True, exist_ok=True)

실습에서는 조지 게어 헨리(George Garr Henry)가 쓴 『How to Invest Money』라는 투자 서적의 텍스트 파일을 예제 문서로 쓴다. 구글 코랩 환경이라면 다음처럼 구글 드라이브에서 파일을 복사해 올 수도 있다.

import os
import shutil
from google.colab import drive

# 구글 드라이브 마운트
drive.mount("/content/drive")

source_path = "/content/drive/MyDrive/langchain-tutorial/Ch05. GraphRAG/Data/How_to_invest_money.txt"
destination_path = "/content/working_directory/input/How_to_invest_money.txt"
shutil.copy(source_path, destination_path)

if os.path.exists(destination_path):
    print(f"파일이 {destination_path}에 성공적으로 복사되었습니다")
else:
    print("파일 복사 실패")

영문 파일을 쓰는 실습이지만 한글 문서를 써도 무방하다. .env에 API 키를 입력하고 폴더 구성이 끝났다면, 다음 명령으로 그래프 DB 구축을 실행한다.

!graphrag index --root ./working_directory

그래프 DB 구축 워크플로우

그래프 DB 구축은 크게 두 범주로 나뉜다.

  • Base processing — 문서 분할부터 엔티티 추출, 기본 그래프·그래프 증강의 초기 버전 구현까지. 초기 형태의 엔티티 그래프를 만들어 이후 단계의 토대를 마련한다.
  • Enrichment(고도화) — 기본 그래프를 정제하고 커뮤니티 요약·시각화를 더해 완성도를 높인다.

이 두 범주는 다음 7단계 워크플로우로 이루어진다.

① 문서 분할(create_base_text_units). 입력 텍스트를 토큰 수 기준으로 청크 분할한다. 분할 기준 토큰 수는 앞선 settings.yamlchunks 설정을 따른다.

② 엔티티 추출(create_base_extracted_entities). 텍스트 조각에서 엔티티를 추출해 기본 엔티티 그래프를 만든다.

③ 기본 엔티티 그래프 처리 및 확장(create_base_entity_graph). 개별 텍스트 유닛에서 만들어진 그래프를 하나로 병합하고(그래프 통합), 유사한 엔티티를 그룹화하며(커뮤니티 탐지), 그래프의 구조적 특성을 벡터화한다(그래프 임베딩 생성). 이 작업의 결과 중 하나는 다음과 같은 형태다.

{
  "id": "GEORGE GARR HENRY",
  "data": {
    "type": "PERSON",
    "description": "George Garr Henry was the Vice-President of Guaranty
      Trust Company of New York and author of 'How to Invest Money' published
      in 1908. ...",
    "cluster": "5",
    "level": "1",
    "degree": "10"
  }
}

cluster는 이 엔티티가 속한 커뮤니티 번호, level은 커뮤니티 계층 구조에서의 위치, degree는 그래프 내에서 다른 엔티티와 맺은 연결 수로 §2의 커뮤니티 요약 우선순위 기준(연결 정도)이 여기서 만들어진다는 것을 보여 준다.

④ 공변량 추출(create_final_covariates). 텍스트 유닛과 엔티티를 바탕으로 §2에서 설명한 주장(claim)을 공변량으로 추출한다.

⑤ 최종 엔티티 데이터 생성(create_final_entities). 기본 엔티티 그래프의 노드 데이터를 정제(중복 제거·빈 값 필터링)하고, 엔티티의 이름·설명에 대한 임베딩을 생성한다. 이 임베딩은 그래프 구조를 임베딩한 Node2vec 결과와는 다른, 엔티티 텍스트 자체를 임베딩한 것이다.

⑥ 최종 노드 데이터 생성 및 레이아웃 계산(create_final_nodes). 엔티티를 시각화하기 위해 노드의 좌표 정보를 계산한다.

참고 — 차원 축소로 노드 좌표 만들기. ① 각 노드의 그래프 임베딩 벡터를 수집한다. ② UMAP(Uniform Manifold Approximation and Projection) 같은 차원 축소 기법으로 고차원 벡터를 2D(또는 3D) 공간으로 변환한다 — 가까운 노드는 2D에서도 가깝게, 먼 노드는 멀게 배치되어 원래 그래프의 구조적 특성이 최대한 보존된다. ③ 축소된 벡터 값을 노드의 x·y 좌표로 할당한다.

이 작업의 결과는 대략 다음과 같다.

{
  "title": "PROJECT GUTENBERG",
  "type": "ORGANIZATION",
  "description": "Project Gutenberg is ... (이하 생략)",
  "community": "10",
  "degree": 23,
  "size": 23,
  "graph_embedding": [-0.0424, ...],
  "x": 16.868,
  "y": 8.587
}

이 단계에서 좌표를 만들려면 반드시 그래프 임베딩 정보가 있어야 하므로, 그래프 시각화를 원한다면 앞선 설정에서 그래프 임베딩 생성을 활성화해야 한다.

⑦ 커뮤니티 요약문 생성(create_final_community_reports). 각 커뮤니티의 요약문을 생성하고 임베딩해 최종 커뮤니티 요약문 테이블을 만든다. 결과는 대략 다음과 같은 형태다.

{
  "community": "0",
  "level": 0,
  "rank": 7.5,
  "title": "Traction Systems and Urban Growth in American Cities",
  "rank_explanation": "The impact severity rating is high due to the critical
    role of traction systems in urban infrastructure and the significant
    financial and legal challenges they face.",
  "summary": "The community is centered around traction systems in major
    American cities ... (이하 생략)",
  "findings": [
    {"explanation": "Traction systems in New York and Chicago are
      experiencing ... (이하 생략)", "summary": "Financial challenges of
      traction systems"}
  ]
}

rank는 요약문의 중요도 점수, findings는 주요 발견 사항 목록(각각 explanation·summary로 구성)이다. 구축이 끝난 결과 파일은 working_directory/output 폴더에 저장된다. 구글 코랩은 런타임이 종료되면 데이터가 사라지므로, 다음처럼 구글 드라이브에 복사해 두는 편이 안전하다.

# working_directory 폴더를 구글 드라이브로 복사
!cp -r /content/working_directory "/content/drive/My Drive/"

4. 그래프 RAG 질의

그래프 DB를 구축했다면 이제 사용자 질문에 답할 차례다. 그래프 RAG는 질문의 성격에 따라 로컬 검색글로벌 검색 중 하나를 골라 쓴다.

로컬 검색

로컬 검색은 "특정 제품의 출시일은 언제인가요?"처럼 문서의 제한된 영역 안에서 답을 찾을 수 있는 명시적인 질문에 적합하다. 동작은 세 단계로 이루어진다.

① 관련 엔티티 추출. 질문을 임베딩하고 그래프 DB에 저장된 엔티티 임베딩과의 유사도를 계산해 연관성 높은 엔티티를 찾는다. "테슬라의 최근 전기차 판매량은 어떻게 되나요?"라고 물으면 '테슬라'·'전기차' 같은 관련 엔티티를 식별한다.

② 연관 정보 수집. 식별된 엔티티를 중심으로 다섯 가지 유형의 정보를 모은다.

  1. 엔티티가 포함된 원본 텍스트 청크
  2. 엔티티가 속한 커뮤니티의 리포트
  3. 엔티티와 연관된 다른 엔티티(예: 테슬라와 연관된 일론 머스크·Model S/3/X/Y)
  4. 엔티티와 연결된 관계(예: 테슬라와 일론 머스크 사이 관계의 강도)
  5. 엔티티에 대한 주장(claim, 예: 테슬라의 2023년 4분기 시장 점유율 하락)

③ 우선순위화 및 필터링. 수집한 정보 중 질문과 실제로 연관된 것만 골라낸다 — 이는 4장에서 다룬 리랭킹과 동일한 과정이다. LLM이 각 정보가 질문과 얼마나 관련 있는지 평가해 가장 관련성 높은 것을 골라 최종 답변을 만든다.

로컬 검색은 단순한 키워드 매칭이나 임베딩 유사도만으로 그치지 않고, 그래프 구조를 활용해 연관된 다양한 컨텍스트를 종합적으로 고려한다는 장점이 있다.

글로벌 검색

글로벌 검색은 "이 문서 전반을 아우르는 주제는 무엇인가요?"처럼 데이터셋 전체를 아우르는 질문에 답한다. 기존 RAG의 벡터 검색은 의미적으로 유사한 텍스트를 찾는 데는 강하지만, 이런 질문에는 애초에 어떤 정보를 검색해야 할지조차 특정하기 어렵다. 글로벌 검색은 LLM이 만든 지식 그래프 구조를 맵-리듀스(map-reduce) 방식으로 활용해 이 한계를 넘는다.

먼저 사용할 커뮤니티 레벨을 고른다. 하위 레벨(Level 2 등)은 더 세부적이지만 처리할 리포트가 많아 시간·자원이 더 든다. 상위 레벨은 세부 정보는 부족해도 더 적은 자원으로 빠르게 답을 만든다.

  • 맵(Map) 단계 — 커뮤니티 리포트들을 작은 텍스트 조각으로 나눠 LLM에 투입해 '중간 응답' 리스트를 만든다. 각 중간 응답에는 최종 답변에 얼마나 중요한지를 나타내는 중요도 점수가 붙는다.
  • 리듀스(Reduce) 단계 — 중요도 점수가 높은 중간 응답들을 선별해 통합하고, 이를 컨텍스트로 삼아 LLM이 종합적인 최종 답변을 작성한다.

실습 — 로컬 검색과 글로벌 검색 비교

앞서 구축한 그래프 DB로 실제 질의를 해 보자. 실습 코드는 5장 폴더의 ch05_GRAPHRAG_질의과정.ipynb다. 글로벌·로컬 검색은 --method 옵션으로 선택한다.

포괄적인 질문: "채권의 전환성 개념이 각 챕터들에서 어떻게 소개되어 있나요?" — 문서 전체에 걸친 여러 투자 수단 정보를 종합해야 답할 수 있는 질문이다.

!graphrag query \
  --root ./ \
  --method global \
  --query "채권의 전환성 개념이 각 챕터들에서 어떻게 소개되어 있나요?"

글로벌 검색은 '전환성의 개념'·'과학적 투자자의 선택 원칙'·'철도 채권과 공공 유틸리티 채권'·'산업 채권과 시장 환경'·'은행과 장비 채권' 등 문서의 주요 주제들을 연결하며 전환성이 각 맥락에서 어떻게 다뤄지는지 일목요연하게 정리해 답한다.

같은 질문을 로컬 검색으로 실행하면(--method local), 전환성이 투자 전략·시장 활동과 맺는 구체적인 관계를 특정 엔티티·관계 데이터([Data: Entities …], [Data: Relationships …])를 근거로 상세히 설명한다. 다만 문서 전체를 아우르는 종합적인 맥락은 글로벌 검색만큼 담아내지 못한다.

세부적인 질문: "산업채권 평가 시 순유동자산(Net Quick Assets)을 어떻게 분석해야 하나요?" — 문서 중에서도 특정 챕터에서 다루는 내용이라 해당 부분을 자세히 참고해야 정확히 답할 수 있는 질문이다.

!graphrag query \
  --root ./ \
  --method local \
  --query "산업채권 평가 시 순유동자산(Net Quick Assets)을 어떻게 분석해야 하나요?"

이번에는 반대의 결과가 나온다 — 글로벌 검색은 순유동자산의 기본 개념과 중요성 정도를 포괄적으로 설명하는 데 그치지만, 로컬 검색은 순유동자산과 유동부채의 비율 분석, 부동산 담보 채권의 안전성 평가 기준처럼 실제로 적용할 수 있는 구체적인 분석 방법을 상세히 제시한다.

두 실습을 비교하면 결론은 명확하다 — 문서 전반의 주제·흐름을 묻는 질문에는 글로벌 검색이, 특정 챕터·세부 사항을 묻는 질문에는 로컬 검색이 더 효과적이다.

5. Neo4j와 랭체인을 활용한 GraphRAG 구현

지금까지는 GraphRAG 라이브러리 자체의 파일 기반 저장소를 썼다. 이 절에서는 그 결과물을 Neo4j라는 그래프 데이터베이스에 영구 저장하고, 랭체인으로 질의에 활용하는 방법을 살펴본다.

참고 — Neo4j란? Neo4j는 전 세계적으로 널리 쓰이는 그래프 데이터베이스 시스템이다. 데이터를 노드(엔티티)와 에지(관계) 구조로 표현하며, Cypher라는 선언적 쿼리 언어로 이를 탐색한다. SQL과 비슷하게 MATCH(패턴 검색)·CREATE(생성)·MERGE(생성 또는 갱신)·SET(속성 설정) 같은 키워드를 쓰지만, 테이블이 아니라 그래프 패턴 매칭으로 노드·관계 사이의 연결을 표현하고 탐색한다는 점이 다르다. Neo4j의 강점은 관계 중심 데이터 모델(복잡한 관계를 자연스럽게 표현·탐색), 고성능 쿼리(대규모 그래프에서도 빠른 응답), 확장성(데이터가 늘어도 안정적인 성능)이다. Cypher를 처음 접한다면 Neo4j 공식 Cypher 소개 문서로 기본 문법을 익히길 권한다.

마이크로소프트 GraphRAG는 문서에서 추출한 엔티티·관계 데이터를 Parquet 파일 형식으로 출력한다(create_final_entities.parquet, create_final_relationships.parquet 등). 이 데이터를 Neo4j에 저장하면 영구적인 그래프 데이터베이스를 구축해 실시간 질의·분석에 활용할 수 있다.

Neo4j Aura 환경 설정

이 책은 클라우드 환경에 그래프 DB를 구축하는 Neo4j Aura를 사용한다. 절차는 다음과 같다. ① Neo4j Aura 공식 홈페이지에서 회원가입 후 [Create instance]로 인스턴스를 생성한다. ② 인스턴스 생성 후 [Download and continue]로 접속 정보(사용자명·비밀번호)를 안전하게 내려받는다 — 비밀번호는 이 시점 이후 다시 확인할 수 없다. ③ 다운로드한 파일에서 다음과 같은 접속 정보를 확인한다.

NEO4J_URI=neo4j+s://...
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=...

GraphRAG 산출물을 Neo4j로 임포트

실습 코드는 5장 폴더의 ch05_GRAPHRAG_NEO4J저장.ipynb다. 먼저 neo4j 패키지를 설치하고, GraphRAG 결과 Parquet 파일이 저장된 구글 드라이브 폴더와 연결한다.

from google.colab import drive
import os

drive.mount("/content/drive")
os.chdir("/content/drive/MyDrive/working_directory")
GRAPHRAG_FOLDER = "/content/drive/MyDrive/working_directory/output"
from neo4j import GraphDatabase
import pandas as pd
import time

NEO4J_URI = "neo4j+s://..."
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "password"
NEO4J_DATABASE = "neo4j"

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))

원문 코드가 추출 과정에서 흐트러져 있었다 — 원문 PDF에서 텍스트를 뽑는 과정에서 이 절의 Cypher·Python 코드는 줄바꿈과 문장이 중복·뒤섞여 있었다. 아래는 원문이 설명하는 로직(배치 임포트 함수 정의 → 중복 방지 제약 조건 생성 → 문서·청크·엔티티·관계·커뮤니티·커뮤니티 보고서 순서로 데이터 임포트)은 그대로 유지하면서, 뜻이 분명한 범위 안에서 코드를 정리한 것이다.

먼저 데이터프레임을 배치 단위로 Neo4j에 적재하는 함수를 정의한다.

def batched_import(statement, df, batch_size=1000):
    """
    데이터프레임을 배치 단위로 Neo4j에 임포트한다.
    statement: 실행할 Cypher 쿼리, df: 임포트할 데이터프레임,
    batch_size: 배치당 임포트할 행 수.
    """
    total = len(df)
    start_s = time.time()
    for start in range(0, total, batch_size):
        batch = df.iloc[start: min(start + batch_size, total)]
        result = driver.execute_query(
            "UNWIND $rows AS value " + statement,
            rows=batch.to_dict("records"),
            database_=NEO4J_DATABASE,
        )
        print(result.summary.counters)
    print(f"{total} rows in {time.time() - start_s} s.")
    return total

다음으로 데이터 중복을 막는 제약 조건을 만든다.

statements = [
    "create constraint chunk_id if not exists for (c:__Chunk__) require c.id is unique",
    "create constraint document_id if not exists for (d:__Document__) require d.id is unique",
    "create constraint community_id if not exists for (c:__Community__) require c.community is unique",
    "create constraint entity_id if not exists for (e:__Entity__) require e.id is unique",
    "create constraint entity_title if not exists for (e:__Entity__) require e.name is unique",
    "create constraint covariate_title if not exists for (e:__Covariate__) require e.title is unique",
    "create constraint related_id if not exists for ()-[rel:RELATED]->() require rel.id is unique",
]
for statement in statements:
    if statement.strip():
        driver.execute_query(statement)

문서 노드 임포트. create_final_documents.parquet에서 문서 데이터를 읽어 __Document__ 노드를 만든다.

doc_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_documents.parquet", columns=["id", "title"]
)
statement = """
MERGE (d:__Document__ {id: value.id})
SET d += value {.title}
"""
batched_import(statement, doc_df)

텍스트 유닛(청크) 임포트. create_final_text_units.parquet에서 청크를 읽어 __Chunk__ 노드를 만들고 문서와 연결한다.

text_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_text_units.parquet",
    columns=["id", "text", "n_tokens", "document_ids"],
)
statement = """
MERGE (c:__Chunk__ {id: value.id})
SET c += value {.text, .n_tokens}
WITH c, value
UNWIND value.document_ids AS document
MATCH (d:__Document__ {id: document})
MERGE (c)-[:PART_OF]->(d)
"""
batched_import(statement, text_df)

엔티티 임포트. create_final_entities.parquet에서 엔티티를 읽어 __Entity__ 노드를 만들고, 엔티티 타입을 라벨로 추가한 뒤 청크와 연결한다.

entity_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_entities.parquet",
    columns=["title", "type", "description", "human_readable_id", "id", "text_unit_ids"],
)
statement = """
MERGE (e:__Entity__ {id: value.id})
SET e.human_readable_id = value.human_readable_id,
    e.description = value.description,
    e.name = coalesce(replace(value.title, '"', ''), 'Unknown')
WITH e, value
CALL apoc.create.addLabels(e,
  CASE WHEN coalesce(value.type, "") = "" THEN []
       ELSE [apoc.text.upperCamelCase(replace(value.type, '"', ''))] END
) YIELD node
WITH e, value
UNWIND value.text_unit_ids AS text_unit
MATCH (c:__Chunk__ {id: text_unit})
MERGE (c)-[:HAS_ENTITY]->(e)
"""
batched_import(statement, entity_df)

관계 임포트. create_final_relationships.parquet에서 관계를 읽어 엔티티 사이에 RELATED 관계를 만든다.

rel_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_relationships.parquet",
    columns=["source", "target", "id", "combined_degree", "weight",
             "human_readable_id", "description", "text_unit_ids"],
)
rel_df = rel_df.rename(columns={"combined_degree": "rank"})
rel_statement = """
MATCH (source:__Entity__ {name: replace(value.source, '"', '')})
MATCH (target:__Entity__ {name: replace(value.target, '"', '')})
MERGE (source)-[rel:RELATED {id: value.id}]->(target)
SET rel += value {.rank, .weight, .human_readable_id, .description, .text_unit_ids}
RETURN count(*) as createdRels
"""
batched_import(rel_statement, rel_df)

커뮤니티 임포트. create_final_communities.parquet에서 커뮤니티를 읽어 __Community__ 노드를 만들고, 관련 청크·엔티티와 연결한다.

community_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_communities.parquet",
    columns=["id", "level", "title", "text_unit_ids", "relationship_ids"],
)
statement = """
MERGE (c:__Community__ {community: value.title})
SET c.title = value.title, c.level = value.level
WITH c, value
UNWIND value.text_unit_ids AS text_unit_id
MATCH (t:__Chunk__ {id: text_unit_id})
MERGE (c)-[:HAS_CHUNK]->(t)
WITH distinct c, value
UNWIND value.relationship_ids AS rel_id
MATCH (start:__Entity__)-[:RELATED {id: rel_id}]->(end:__Entity__)
MERGE (start)-[:IN_COMMUNITY]->(c)
MERGE (end)-[:IN_COMMUNITY]->(c)
RETURN count(distinct c) as createdCommunities
"""
batched_import(statement, community_df)

커뮤니티 보고서 임포트. create_final_community_reports.parquet에서 요약 보고서를 읽어 커뮤니티 노드에 속성으로 반영하고, 발견 사항을 개별 Finding 노드로 저장한다.

community_report_df = pd.read_parquet(
    f"{GRAPHRAG_FOLDER}/create_final_community_reports.parquet",
    columns=["id", "community", "level", "title", "summary", "findings",
             "rank", "rank_explanation", "full_content"],
)
community_report_df["community"] = "Community" + community_report_df["community"].astype(str)
community_statement = """
MERGE (c:__Community__ {community: value.community})
SET c.level = value.level,
    c.name = value.title,
    c.rank = value.rank,
    c.rank_explanation = value.rank_explanation,
    c.full_content = value.full_content,
    c.summary = value.summary
WITH c, value
UNWIND range(0, size(value.findings) - 1) AS finding_idx
WITH c, value, finding_idx, value.findings[finding_idx] AS finding
MERGE (c)-[:HAS_FINDING]->(f:Finding {id: finding_idx})
SET f += finding
"""
batched_import(community_statement, community_report_df)

마지막으로 create_final_nodes.parquet의 엔티티-커뮤니티 소속 정보 중 앞선 단계에서 누락된 것을 보완해 연결한다. 이 과정을 거치면 GraphRAG의 결과물이 Neo4j Aura에 온전히 저장되어 실시간 질의·분석에 쓸 준비가 끝난다.

랭체인으로 검색기 구현

Neo4j에 그래프 DB를 구축했으니, 이제 랭체인으로 로컬 검색글로벌 검색을 직접 구현해 본다. 실습 코드는 5장 폴더의 ch05_GRAPHRAG_NEO4J_RETRIEVER.ipynb다.

!pip install langchain langchain-neo4j langchain-openai langchain_community
from langchain_neo4j import Neo4jGraph
from langchain_community.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from dotenv import load_dotenv

load_dotenv("/content/.env")

NEO4J_URI = "neo4j+s://..."
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "password"
NEO4J_DATABASE = "neo4j"

embedding = OpenAIEmbeddings()

# 엔티티 설명을 벡터 검색 대상으로 삼는 벡터 스토어
graph = Neo4jVector.from_existing_graph(
    embedding=embedding,
    node_label="__Entity__",
    text_node_properties=["description"],
    embedding_node_property="embedding",
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD,
)

# Cypher 쿼리 실행용 그래프 객체
neo4j_graph = Neo4jGraph(
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD,
    database=NEO4J_DATABASE,
)

로컬 검색 구현. 먼저 엔티티와 연관된 정보를 수집하는 함수를 정의한다. §4의 「연관 정보 수집」 단계(텍스트 청크·커뮤니티 보고서·관련 엔티티)를 코드로 옮긴 것이다.

def fetch_entity_context(entity_name):
    context = {"name": entity_name}
    try:
        chunk_query = """
        MATCH (e:__Entity__ {name: $entity_name})<-[:HAS_ENTITY]-(c:__Chunk__)
        RETURN c.text AS text
        """
        chunk_result = neo4j_graph.query(chunk_query, {"entity_name": entity_name})
        context["text_chunks"] = [r["text"] for r in chunk_result] or ["No text chunk available"]

        community_query = """
        MATCH (e:__Entity__ {name: $entity_name})-[:IN_COMMUNITY]->(com:__Community__)
        RETURN com.full_content AS report
        """
        community_result = neo4j_graph.query(community_query, {"entity_name": entity_name})
        context["community_reports"] = [r["report"] for r in community_result] or ["No community report available"]

        related_query = """
        MATCH (e:__Entity__ {name: $entity_name})-[:RELATED]->(related:__Entity__)
        RETURN related.name AS name, related.description AS description
        """
        related_result = neo4j_graph.query(related_query, {"entity_name": entity_name})
        context["related_entities"] = [
            {"name": r["name"], "description": r["description"]} for r in related_result
        ]
    except Exception as e:
        context["error"] = f"Error fetching context: {str(e)}"
    return context

수집한 정보는 다음 함수로 하나의 읽기 쉬운 컨텍스트 문자열로 정리한다.

def create_structured_context(all_contexts, query):
    context_str = "## 질문과 관련된 엔티티 정보\n\n"
    context_str += "아래는 질문에 답변하는 데 유용한 엔티티들의 구조화된 정보입니다:\n\n"
    for i, ctx in enumerate(all_contexts, 1):
        context_str += f"### 엔티티 {i}: {ctx['name']}\n"
        context_str += f"- **설명**: {ctx['description']}\n"
        context_str += "- **텍스트 청크**:\n"
        for chunk in ctx["text_chunks"]:
            context_str += f"  - {chunk}\n"
        context_str += "- **커뮤니티 보고서**:\n"
        for report in ctx["community_reports"]:
            context_str += f"  - {report}\n"
        if ctx["related_entities"]:
            context_str += "- **관련 엔티티**:\n"
            for rel in ctx["related_entities"]:
                context_str += f"  - {rel['name']}: {rel['description']}\n"
        else:
            context_str += "- **관련 엔티티**: 없음\n"
        context_str += "\n"
    return context_str

이제 LLM과 리트리버를 설정하고 전체 흐름을 연결한다.

llm = ChatOpenAI(model="gpt-4o")   # 책 집필 시점 예시 모델명. 최신 모델 지정은 이 장 끝 최신 동향 참고
retriever = graph.as_retriever(search_type="similarity", search_kwargs={"k": 3})

query = "마일당 순이익(NET INCOME PER MILE)을 어떻게 분석해야 하나요?"
results = retriever.get_relevant_documents(query)

all_contexts = []
for result in results:
    entity_name = result.metadata.get("name", "Unknown")
    context = fetch_entity_context(entity_name)
    context["name"] = entity_name
    context["description"] = result.page_content
    all_contexts.append(context)

context_str = create_structured_context(all_contexts, query)
prompt = f"아래 맥락에 기반해서, 주어진 질문에 한국어로 답하세요\n\n**질문**: {query}\n\n**맥락**:\n{context_str}"

response = llm.invoke(prompt)
print(response.content)

이 흐름은 벡터 스토어에서 질문과 유사한 엔티티를 찾고(리트리버), 각 엔티티의 청크·커뮤니티 보고서·관련 엔티티까지 모은 뒤(fetch_entity_context), 구조화된 컨텍스트로 정리해(create_structured_context) LLM에 넘겨 답을 생성한다 — §4에서 설명한 로컬 검색의 세 단계를 그대로 코드로 옮긴 것이다.

글로벌 검색 구현. 글로벌 검색은 맵-리듀스 방식을 그대로 두 개의 프롬프트·체인으로 구현한다.

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from tqdm import tqdm

llm = ChatOpenAI(model="gpt-4o")

MAP_SYSTEM_PROMPT = """
---역할---
제공된 컨텍스트를 참고하여 사용자의 질문에 답하는 어시스턴트입니다.
---목표---
주어진 컨텍스트가 질문을 답하기에 적절하다면 질문에 대한 답을 한 뒤, 답변의 중요도 점수를 기입하여
JSON 형식으로 생성하세요. 정보가 부족하면 "모르겠습니다"라고 답하세요.
각 포인트는 다음을 포함해야 합니다:
- 답변: 질문에 대한 답변
- 중요도 점수: 0~100 사이의 정수
데이터 참조 예: "예시 문장 [Data: Reports (2, 7, 64, 46, 34, more)]" (한 참조에 5개 이상의 id는 "+more" 사용)
출력 예: {{"Answer": "답변 [Data: Reports (보고서 id들)]", "score": 점수}}
"""
map_prompt = ChatPromptTemplate.from_messages([
    ("system", MAP_SYSTEM_PROMPT),
    ("human", "question: {question}\n\ncontext: {context}"),
])
map_chain = map_prompt | llm | StrOutputParser()

REDUCE_SYSTEM_PROMPT = """
---역할---
맵 단계에서 처리된 여러 결과를 종합하여 사용자의 질문에 답하는 어시스턴트입니다.
---목표---
제공된 맵 단계 결과를 바탕으로, 질문에 대한 종합적인 답변을 마크다운 형식으로 작성하세요.
중요도 점수를 고려하여 핵심적인 결과 위주로 반영하며, 불필요한 세부 사항은 제외하세요.
---맵 단계 결과---
{report_data}
데이터 참조 형식은 아래를 따르세요: "예시 문장 [Data: Reports (2, 7, 34, 46, 64, more)]"
대상 응답 길이 및 형식: {response_type}
"""
reduce_prompt = ChatPromptTemplate.from_messages([
    ("system", REDUCE_SYSTEM_PROMPT),
    ("human", "{question}"),
])
reduce_chain = reduce_prompt | llm | StrOutputParser()

response_type = "multiple paragraphs"

def global_retriever(query: str, level: int, response_type: str = response_type) -> str:
    community_data = neo4j_graph.query(
        """
        MATCH (c:__Community__)
        WHERE c.level = $level
        RETURN c.full_content AS output
        """,
        params={"level": level},
    )
    intermediate_results = []
    for community in tqdm(community_data, desc="Processing communities"):
        intermediate_response = map_chain.invoke(
            {"question": query, "context": community["output"]}
        )
        intermediate_results.append(intermediate_response)
    return reduce_chain.invoke({
        "report_data": intermediate_results,
        "question": query,
        "response_type": response_type,
    })

print(global_retriever("이 책의 주제가 뭐야?", 1))

global_retriever 함수는 §4의 맵-리듀스 절차를 그대로 옮긴다 — 지정한 레벨의 커뮤니티 리포트를 모두 가져와(neo4j_graph.query) 각각에 맵 체인을 적용해 중간 응답을 만들고(맵 단계), 그 결과를 리듀스 체인에 모아 최종 답을 만든다(리듀스 단계). 이 질의를 실행하면 "이 책의 주제는 금융 시장의 복잡한 상호작용과 다양한 금융 상품들이 경제·투자 환경에서 어떻게 연결되는지를 다룬다"는 식으로, 문서 전체의 흐름을 반영한 답이 돌아온다.

이렇게 Neo4j와 랭체인을 결합하면 GraphRAG의 산출물을 일회성 파일이 아니라 영구적인 그래프 데이터베이스로 다루면서, 로컬·글로벌 검색을 애플리케이션 코드 안에서 자유롭게 조합할 수 있다.

핵심 개념 정리

개념 한 줄 설명
기존 RAG의 두 한계 정보 연결의 어려움 · 대규모 데이터 이해의 한계 — 둘 다 "조각을 독립적으로만 검색한다"는 원인에서 나온다
지식 그래프 노드·에지에 의미(타입·속성)를 부여한 그래프. 복잡한 정보 구조·추론·효율적 검색이라는 세 특징을 갖는다
그래프 DB 구축 4단계 문서 분할 → 지식 그래프 구축 → 그래프 증강 → 커뮤니티 요약
엔티티·관계 추출 방법의 변천 규칙 기반 → 머신러닝(SVM·CRF) → 딥러닝(RNN·트랜스포머) → LLM(현재 주류)
Gleaning LLM 추출 결과를 반복 검증·보완해 누락된 엔티티·관계를 줄이는 기법
커뮤니티 탐지 밀접하게 연관된 엔티티를 그룹화. 로우벤(빠르지만 불안정) · 레이든(연결 보장, 더 안정적)
계층적 커뮤니티 레이든으로 만드는 Level 0~2 구조. 레벨이 깊을수록 세부적, 얕을수록 포괄적
그래프 임베딩 vs 엔티티(텍스트) 임베딩 전자는 그래프 구조(Node2vec), 후자는 엔티티 설명 텍스트의 의미를 벡터화 — 대상이 다르다
커뮤니티 요약 우선순위 연결 정도(degree) 기준으로 정렬한 뒤 순서대로 LLM에 투입해 요약 생성
GraphRAG 라이브러리 워크플로우 Base processing(분할·추출·기본그래프) → Enrichment(정제·요약·시각화), 7단계로 세분
로컬 검색 질문 임베딩 → 관련 엔티티 식별 → 5종 정보 수집 → 리랭킹. 세부 질문에 강함
글로벌 검색 커뮤니티 레벨 선택 → 맵(중간 응답) → 리듀스(통합). 포괄 질문에 강함
Neo4j·Cypher 그래프 전용 데이터베이스·쿼리 언어. GraphRAG의 Parquet 산출물을 영구 저장하는 데 쓴다
랭체인 검색기 구현 로컬 = 벡터 유사도 + fetch_entity_context, 글로벌 = Neo4jGraph 쿼리 + 맵-리듀스 체인

실무 체크리스트

  • [ ] 이 데이터에 정말 "조각 사이의 관계"가 중요한가, 아니면 단순 벡터 검색(2장)만으로 충분한가?
  • [ ] 문서 분할 크기를 정할 때 비용(청크 수)과 엔티티 추출 품질의 트레이드오프를 함께 고려했는가?
  • [ ] 엔티티 추출에 Gleaning을 쓸 경우, 반복 횟수 증가가 비용 대비 실제로 품질을 개선하는지 확인했는가?
  • [ ] 커뮤니티 탐지 알고리즘을 고를 때 속도(로우벤)와 안정성(레이든) 중 이 프로젝트에 맞는 쪽을 판단했는가?
  • [ ] 계층적 커뮤니티를 쓴다면, 질문의 범위·깊이에 맞는 레벨을 선택하는 로직을 두었는가?
  • [ ] 질문이 특정 세부 사항을 묻는지, 문서 전체를 아우르는지 먼저 판별해 로컬/글로벌 검색을 선택했는가?
  • [ ] 그래프 임베딩(구조)과 엔티티 텍스트 임베딩(의미)을 혼동하지 않고 각각 올바른 용도로 썼는가?
  • [ ] GraphRAG의 Parquet 산출물을 실시간 서비스에 쓸 계획이라면, Neo4j 같은 영구 저장소로 옮겼는가?
  • [ ] Neo4j에 데이터를 임포트할 때 제약 조건(unique 등)으로 중복 노드·관계 생성을 막았는가?
  • [ ] 그래프 RAG 구축·질의 비용(LLM 호출 횟수)이 이 프로젝트의 예산·응답 시간 요구에 맞는지 가늠했는가?

연습문제

  1. 검색 방식 선택. 사내 정책 문서 100건에 대해 "출장 여비 상한선은 얼마인가?"와 "우리 회사의 리모트 근무 정책은 지난 3년간 어떻게 변해 왔는가?"라는 두 질문이 들어온다. 각각 로컬 검색과 글로벌 검색 중 어느 쪽이 더 적합한지 근거를 들어 설명하라.
  2. 커뮤니티 탐지 알고리즘 선택. 매일 수만 건씩 새로 쌓이는 뉴스 기사로 지식 그래프를 만드는 서비스를 설계한다. 커뮤니티를 하루에 한 번씩 다시 계산해야 한다면, 로우벤과 레이든 알고리즘 중 어느 쪽을 고를지와 그 이유를 제시하라.
  3. 비용-품질 트레이드오프 설계. 예산이 한정된 스타트업이 100만 토큰 분량의 사내 문서로 그래프 DB를 구축하려 한다. 문서 분할 크기와 Gleaning 반복 횟수를 어떻게 설정할지, 그리고 그 설정이 만들어 낼 결과(엔티티 수·품질·비용)의 트레이드오프를 설명하라.
  4. 아키텍처 설계. 마이크로소프트 GraphRAG로 그래프 DB를 구축한 뒤, 이를 실시간 챗봇 서비스에 연동하려 한다. GraphRAG의 파일 기반 산출물을 그대로 쓰는 것과 Neo4j로 옮겨 쓰는 것의 차이를 응답 속도·동시 사용자 수·데이터 갱신 빈도 관점에서 비교하라.

최신 동향 (2026-09 기준)

최신 동향 (검증 2026-09-12) — 그래프 RAG의 개념·지식 그래프 구조·로컬/글로벌 검색 원리는 이 장의 설명대로 유효하다. 아래는 책이 실습에 쓴 구체적인 패키지들의 이후 변화다. (검증: Tavily+Brave 교차 확인, SearXNG는 세 질의 모두 무관 결과만 반환해 미반영 — 실질 N=2)

  • 책이 실습에 쓴 마이크로소프트 GraphRAG 라이브러리 자체는 "활발한 개발"에서 유지보수 모드로 전환됐다. 공식 저장소는 "이 프로젝트는 대체로 유지보수 모드이며, 새 PR을 받거나 새 기능을 구현하지 않는다. CVE 대응 등 필요한 버그 수정과 의존성 갱신만 수행한다"고 명시한다 — §3에서 원문이 "지금도 활발히 개발되고 있는 패키지"라 소개한 것과 달라진 지점이다. 마이크로소프트 리서치는 이후 인덱싱 비용을 크게 줄인 후속 연구(LazyGraphRAG 등)로 무게중심을 옮겼다. 최신 상태는 microsoft/graphrag 공식 저장소에서 확인할 수 있다.
  • Neo4j가 §5의 수작업 Parquet→Cypher 임포트를 대체할 공식 패키지를 내놓았다. neo4j-graphrag-python은 Neo4j가 직접 유지보수하는 퍼스트파티 패키지로, 지식 그래프 구축·검색·전체 GraphRAG 파이프라인을 한 곳에서 제공한다 — 이 장 §5에서 손으로 짠 배치 임포트·제약 조건 생성 코드를 라이브러리 호출로 단순화할 수 있다는 뜻이다. 자세한 내용은 Neo4j GraphRAG for Python 공식 문서를 참고하라.
  • langchain-neo4j는 책이 쓴 그대로 현재도 공식 지원 경로다. 이 패키지는 랭체인 v0.2부터 도입된 "파트너 패키지" 체계 아래 Neo4j가 공식으로 공동 유지보수하는 통합이며, §5의 from langchain_neo4j import Neo4jGraph 임포트 방식이 여전히 권장되는 방법이다. 공식 발표는 Neo4j 블로그: LangChain-Neo4j 파트너 패키지에 있다.
  • 책의 코드 예시가 지정한 모델명(gpt-4-turbo-preview, gpt-4o)은 집필 시점 기준이다. 실제 프로젝트에서는 각 제공업체의 공식 모델 문서에서 현재 사용 가능한 모델을 확인해 지정하는 편이 안전하다 — OpenAI 모델 문서 참고.

부록 A. 핵심 비교표

비교 1. 로컬 검색 vs 글로벌 검색

항목 로컬 검색(Local Search) 글로벌 검색(Global Search)
적합한 질문 특정 사실·세부 정보를 묻는 질문 문서 전체를 아우르는 포괄적 질문
핵심 절차 질문 임베딩 → 관련 엔티티 식별 → 5종 정보 수집 → 리랭킹 커뮤니티 레벨 선택 → 맵(중간 응답) → 리듀스(통합)
주로 참조하는 데이터 텍스트 청크·엔티티·관계·주장(claim) 커뮤니티 요약 리포트
장점 그래프 구조로 연관 컨텍스트를 종합적으로 고려 데이터셋 전체 구조·주제를 파악해 종합 답변
한계 질문 범위가 넓으면 필요한 정보를 다 못 담을 수 있음 세부적인 분석·구체적 수치 제공에는 약함

비교 2. 로우벤 알고리즘 vs 레이든 알고리즘

항목 로우벤 알고리즘(Louvain) 레이든 알고리즘(Leiden)
등장 시기 로우벤이 먼저 2018년, 로우벤의 개선판
핵심 동작 모듈성 변화를 반복 계산해 노드를 이동 이동 후에도 커뮤니티 내부 연결을 항상 보장 + 정제 단계 추가
대표 문제 해결 로우벤에서 발생하던 "비연결 커뮤니티" 문제 해결
장점 계산이 빠르고 구현이 간단 더 세밀하고 안정적인 커뮤니티, 적은 반복으로 좋은 결과
단점 초기 설정·처리 순서에 따라 결과 불안정 정제 단계로 반복당 계산량 증가 가능

비교 3. 그래프 임베딩 vs 엔티티 텍스트 임베딩

항목 그래프 임베딩(Node2vec) 엔티티 텍스트 임베딩
임베딩 대상 그래프의 구조적 특성(노드 간 연결 관계) 엔티티의 이름·설명(자연어 텍스트)
만드는 방법 랜덤워크로 이웃 노드 집합(컨텍스트) 수집 후 벡터화 일반적인 텍스트 임베딩 모델로 설명 문장을 벡터화
생성 시점(§3) create_base_entity_graph(그래프 증강 단계) create_final_entities(최종 엔티티 데이터 생성 단계)
주 용도 그래프 시각화 좌표 계산(UMAP 차원 축소의 입력) 질문과 엔티티 사이의 벡터 유사도 검색(§5 리트리버)

부록 B. 추천 참고 자료

외부 자료 (Tier 1 공식·원논문, 생존 확인 2026-09-12)

더 해보기 — 읽고 끝내지 않으려면

  • 같은 질문 하나를 --method local--method global로 각각 실행해, §4에서 정리한 차이(세부 vs 포괄)가 실제로 나타나는지 직접 비교해 본다.
  • §2에서 배운 로우벤·레이든 두 알고리즘을 작은 예제 그래프(노드 10~20개)에 각각 적용해, 커뮤니티 구성이 실제로 어떻게 달라지는지 관찰한다.
  • 문서 분할 크기(청크 토큰 수)를 300 → 600 → 1200으로 바꿔 가며 그래프 DB를 다시 만들어 보고, 감지되는 엔티티 수와 구축 시간·비용이 어떻게 변하는지 §2의 트레이드오프 설명과 대조한다.

본 책 연계 챕터

챕터 이 장이 다루지 않은 것
2장 §4 벡터 데이터베이스 자체의 구조·검색 방식 — 이 장의 로컬 검색이 쓰는 임베딩 유사도 검색의 기반
4장 §4 문서 후처리(리랭킹)의 세부 방법(LLM 기반·크로스 인코더) — 이 장 §4의 「우선순위화 및 필터링」이 전제하는 내용
9장 §1 임베딩 모델 자체를 학습·파인튜닝하는 원리 — 이 장이 쓰는 엔티티 텍스트 임베딩은 기성 모델을 그대로 호출할 뿐, 모델을 학습시키는 방법은 다루지 않는다

부록 C. 연습문제 풀이

  1. (문제 1 정답) "출장 여비 상한선은 얼마인가?"는 특정 규정 하나만 찾으면 되는 명시적 질문이므로 로컬 검색이 적합하다 — 관련 엔티티(출장비 규정)를 식별해 그 주변 텍스트 청크·관계만 모아도 충분한 답이 나온다. 반대로 "리모트 근무 정책이 지난 3년간 어떻게 변해 왔는가?"는 여러 시점의 문서에 흩어진 변화를 종합해야 하는 질문이므로 글로벌 검색이 적합하다 — 개별 정책 문서 하나만 봐서는 "변화의 흐름"이라는 전체 패턴을 파악할 수 없고, 커뮤니티 요약을 맵-리듀스로 종합해야 한다.
  2. (문제 2 정답) 매일 대량의 새 기사가 들어와 커뮤니티를 자주(하루 한 번) 다시 계산해야 한다면 로우벤 알고리즘이 더 적합하다. 로우벤은 계산이 빠르고 구현이 간단해 대규모 데이터를 짧은 주기로 반복 처리하는 데 유리하다. 다만 결과가 실행마다 다소 불안정할 수 있다는 점은 감수해야 한다. 반대로 커뮤니티 구조의 정확성·안정성이 서비스 품질에 직결되고 재계산 주기가 길어도 된다면(예: 주 1회) 레이든이 낫다 — 이 문제처럼 "매일 재계산"이라는 속도 제약이 있다면 로우벤 쪽으로 기운다.
  3. (문제 3 정답) 예산이 한정적이라면 문서 분할 크기를 상대적으로 크게(예: 1200~2400 토큰) 잡아 LLM 호출 횟수와 비용을 줄이는 것이 출발점이다. 다만 §2에서 확인했듯 청크를 키우면 감지되는 엔티티 수가 줄어 지식 그래프 품질이 낮아지므로, 이를 보완하려면 Gleaning 반복 횟수를 2~3회로 늘려 놓치는 엔티티를 추가로 잡아낸다. 즉 "큰 청크 + Gleaning 보완"으로 비용은 낮추고 품질 저하는 완화하는 절충이 가능하다 — Gleaning을 늘리면 그만큼 LLM 호출이 다시 늘어나므로, 예산 안에서 청크 크기와 Gleaning 횟수를 함께 조정해야 한다.
  4. (문제 4 정답) GraphRAG의 파일(Parquet) 기반 산출물을 그대로 쓰면 별도 인프라 없이 빠르게 시작할 수 있지만, 매 질의마다 파일을 읽어야 해 동시 사용자가 늘수록 응답 속도가 떨어지고 데이터 갱신(새 문서 추가)마다 전체 파이프라인을 다시 돌려야 한다. Neo4j로 옮기면 §5처럼 실시간 질의에 최적화된 인덱스·쿼리 엔진을 쓸 수 있어 동시 사용자 수가 많은 서비스에 유리하고, 새 노드·관계를 배치 임포트로 점증적으로 추가할 수 있어 갱신도 더 유연하다. 다만 Neo4j 인스턴스 운영이라는 추가 비용·복잡도가 따르므로, 트래픽이 적은 프로토타입 단계에서는 파일 기반으로 충분할 수 있다.
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플   P 음성 버전 2026-08-05b